package tree;

import java.awt.Point;
import java.awt.Toolkit;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragGestureRecognizer;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.HashMap;

import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JTree;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultTreeCellEditor;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import ui.Main;

/**
 * Class that manages the tree of chapters/pages.
 * @author TKington
 *
 */
public class PageTree extends JTree 
			implements DragGestureListener, DragSourceListener, DropTargetListener {
	/** The one and only instance */
	private static PageTree instance;
	
	/**
	 * Returns the single instance.
	 * @return the instance
	 */
	public static PageTree instance() {
		if(instance == null)
			instance = new PageTree();
		return instance;
	}
	
	/** The tree model. */
	private DefaultTreeModel model;
	/** The currently selected node. */
	private PageNode curNode;
	/** List of ITreeListeners. */
	private ArrayList<ITreeListener> listeners;
	/** Map from page ID to PageNode. */
	private HashMap<Integer, PageNode> pageMap;
	/** Should events be sent to the listeners? */
	private boolean sendEvents = true;
	
	/** DragSource to start drag and drop operations. */
	private DragSource dragSource;
	
	/**
	 * Creates the PageTree instance.
	 *
	 */
	private PageTree() {
		createPopupMenus();
        listeners = new ArrayList<ITreeListener>();
        pageMap = new HashMap<Integer, PageNode>();
        
        model = (DefaultTreeModel)getModel();
        PageNode root = new PageNode("");
        model.setRoot(root);
        
        setShowsRootHandles(true);
        getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
        putClientProperty("JTree.lineStyle", "Angled");
        
        setEditable(true);
        DefaultTreeCellEditor editor = 
        	new DefaultTreeCellEditor(this, 
        			(DefaultTreeCellRenderer)getCellRenderer());
        setCellEditor(editor);
    
        addTreeSelectionListener(new TreeSelectionListener() {
            public void valueChanged(TreeSelectionEvent e) {
                onTreeSelChanged();
            }
        });

        addMouseListener(new MouseAdapter() {
            @Override
			public void mousePressed(MouseEvent e) {
                onTreeMousePressed(e);
            }
        });
        
        model.addTreeModelListener(new TreeModelListener() {
        	public void treeNodesInserted(TreeModelEvent e) {
				Object [] newNodes = e.getChildren();
				for (int i = 0; i < newNodes.length; i++) {
					PageNode node = (PageNode)newNodes[i];
					addToMap(node);
				}
				
				if(sendEvents) {
					for(ITreeListener l : listeners)
						l.treeChanged();
				}
			}

			public void treeNodesRemoved(TreeModelEvent e) {
				Object [] removedNodes = e.getChildren();
				for(int i = 0; i < removedNodes.length; i++) {
					PageNode page = (PageNode)removedNodes[i];
					removeFromMap(page);
				}
				
				if(sendEvents) {					
					for(ITreeListener l : listeners)
						l.treeChanged();
				}
			}

			public void treeStructureChanged(TreeModelEvent e) {
				pageMap.clear();
				addToMap(getRoot());
				
				if(sendEvents) {
					for(ITreeListener l : listeners)
						l.treeChanged();
				}
			}

			public void treeNodesChanged(TreeModelEvent e) {
				pageMap.clear();
				addToMap(getRoot());
				
				if(sendEvents) {
					for(ITreeListener l : listeners)
						l.treeChanged();
				}
        	}
        });
        
        dragSource = DragSource.getDefaultDragSource();
        DragGestureRecognizer dgr = 
        	dragSource.createDefaultDragGestureRecognizer(
        			this, //DragSource
        			DnDConstants.ACTION_MOVE, //specifies valid actions
        			this);//DragGestureListener

        dgr.setSourceActions(dgr.getSourceActions() & ~InputEvent.BUTTON3_MASK);

        new DropTarget(this, this);
	}
	
	/**
	 * Gets a PageNode by id.
	 * @param id the id
	 * @return the PageNode with the matching id
	 */
	public PageNode getPage(int id) {
		return pageMap.get(id);
	}

	/**
	 * Gets a PageNode by name.
	 * @param name the name
	 * @return the PageNode with the given name
	 */
	public PageNode getPage(String name) {
		for(PageNode page : pageMap.values()) {
			if(name.equals(page.getUserObject())) {
				return page;
			}
		}
		
		return null;
	}
	
	/**
	 * Returns the selected page.
	 * @return the selected page.
	 */
	public PageNode getSelectedPage() {
		return (PageNode)getSelectionPath().getLastPathComponent();
	}
	
	/**
	 * Adds a PageNode to the id->PageNode map.
	 * @param node the PageNode to be added
	 */
	private void addToMap(PageNode node) {
		pageMap.put(node.getID(), node);
		for(int i = 0; i < node.getChildCount(); i++) {
			PageNode child = (PageNode)node.getChildAt(i);
			addToMap(child);
		}
	}
	
	/**
	 * Removes a PageNode from the map.
	 * @param node the node to be removed
	 */
	private void removeFromMap(PageNode node) {
		pageMap.remove(node.getID());
		for(int i = 0; i < node.getChildCount(); i++) {
			PageNode child = (PageNode)node.getChildAt(i);
			removeFromMap(child);
		}
	}
	
	/**
	 * Handler for TreeSelChanged events.  Notifies listeners that
	 * the selection has changed.
	 */
	private void onTreeSelChanged() {
		TreePath path = getSelectionPath();
		if(path == null)
			return;
		
		curNode = (PageNode)(path.getLastPathComponent());
		
		if(sendEvents) {
			for(ITreeListener l : listeners)
				l.treeSelectionChanged(curNode);
		}
	}
	
	/**
	 * Sets the currently selected page without notifying the listeners.
	 * @param dest the PageNode to be selected.
	 */
	public void setSelectionWithoutEvents(PageNode dest) {
		if(curNode != dest) {
			sendEvents = false;
			setSelectionPath(new TreePath(dest.getPath()));
			sendEvents = true;
		}
	}
	
	/**
	 * Handles TreeMousePressed events to display pop-up menus.
	 * @param e the event
	 */
	private void onTreeMousePressed(MouseEvent e) {
        if((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0)
        {
        	TreePath selPath = getPathForLocation(e.getX(), e.getY());
        	
        	if(selPath == null)
        		return;
        	
        	setSelectionPath(selPath);
        	
            JPopupMenu popup = curNode.getPopupMenu();
            popup.show(e.getComponent(), e.getX(), e.getY());
        }
	}

	/**
	 * Creates a new child page.
	 * @param parent the parent of the new page
	 */
	private void onNewChild(PageNode parent) {
		PageNode node = new PageNode("A Page");
		model.insertNodeInto(node, parent, parent.getChildCount());
		scrollPathToVisible(new TreePath(node.getPath()));
	}
	
	/**
	 * Deletes a PageNode.
	 * @param node the node to be deleted
	 */
	private void onDelete(PageNode node) {
		PageNode parent = (PageNode)node.getParent();
		if(parent == null) {
			Toolkit.getDefaultToolkit().beep();
			return;
		}
		
		int index = parent.getIndex(node);
		node.removeFromParent();
		model.nodesWereRemoved(parent, new int[] {index}, new Object [] {node});
		setSelectionPath(new TreePath(parent.getPath()));
	}
	
	/**
	 * Creates the pop-up menus.
	 *
	 */
	private void createPopupMenus() {
		JPopupMenu pageMenu = new JPopupMenu();
		
        JMenuItem mi = new JMenuItem("New Child");
        mi.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
            	onNewChild(curNode);
            }
        });
        pageMenu.add(mi);
        
        mi = new JMenuItem("Delete");
        mi.addActionListener(new ActionListener() {
        	public void actionPerformed(ActionEvent e) {
        		onDelete(curNode);
        	}
        });
        pageMenu.add(mi);
        
        PageNode.setPopupMenu(pageMenu);
	}
	
	/**
	 * Sets the root of the tree.
	 * @param root the new root
	 */
	public void setRoot(PageNode root) {
		model.setRoot(root);
	}
	
	/**
	 * Returns the root of the tree.
	 * @return the root
	 */
	public PageNode getRoot() { return (PageNode)model.getRoot(); }
	
	/**
	 * Adds an ITreeListener.
	 * @param l the new listener
	 */
	public void addTreeListener(ITreeListener l) { listeners.add(l); }
	
	/**
	 * Removes an ITreeListener.
	 * @param l the listener to be removed
	 */
	public void removeTreeListener(ITreeListener l) { listeners.remove(l); }

	/**
	 * Inserts a blank page after the currently selected page.
	 *
	 */
	public void insertBlankNode() {
		PageNode node = new PageNode("A Page");
		PageNode parent = (PageNode)curNode.getParent();
		
		if(parent == null) {
			Toolkit.getDefaultToolkit().beep();
			return;
		}
		
		model.insertNodeInto(node, parent, 
				model.getIndexOfChild(parent, curNode) + 1);
		scrollPathToVisible(new TreePath(node.getPath()));
		
		Main.frame.gotoPage(node, true);
	}

	/**
	 * Inserts a copy of the selected page immediately after the selected page.
	 *
	 */
	public void insertNodeCopy() {
		PageNode node = new PageNode(curNode);
		PageNode parent = (PageNode)curNode.getParent();
		
		if(parent == null) {
			Toolkit.getDefaultToolkit().beep();
			return;
		}
		
		model.insertNodeInto(node, parent, 
				model.getIndexOfChild(parent, curNode) + 1);
		scrollPathToVisible(new TreePath(node.getPath()));
		
		Main.frame.gotoPage(node, true);
	}

	//	DragGestureListener
	public void dragGestureRecognized(DragGestureEvent dge) {
		if(!Main.frame.editMode)
			return;
		
		if(curNode != null && curNode != getRoot()) {
			Transferable trans = curNode;
			dragSource.startDrag(dge, null, trans, this);
		}
	}

	//////////////////////////////
	//	DragSourceListener methods
	public void dragDropEnd(DragSourceDropEvent dsde) { /* */ }
	public void dragEnter(DragSourceDragEvent dsde) { /* */ }
	public void dragExit(DragSourceEvent dse) { /* */ }
	public void dragOver(DragSourceDragEvent dsde) { /* */ }
	public void dropActionChanged(DragSourceDragEvent dsde) { /* */ }

	//////////////////////////////
	//	DragTargetListener methods
	public void drop(DropTargetDropEvent dtde) {
		Transferable trans = dtde.getTransferable();
		
		if(!trans.isDataFlavorSupported(PageNode.pageNodeFlavor)) {
			dtde.rejectDrop();
			return;
		}
				
		Point loc = dtde.getLocation();
		TreePath destPath = getPathForLocation(loc.x, loc.y);
		PageNode destNode = (PageNode)destPath.getLastPathComponent();
		
		//	Don't allow drop on root
		if(destNode == getRoot()) {
			dtde.rejectDrop();
			return;
		}
		
		if(dtde.getDropAction() == DnDConstants.ACTION_MOVE)
			dtde.acceptDrop(DnDConstants.ACTION_MOVE);
		
		model.removeNodeFromParent(curNode);
		
		PageNode destParent = (PageNode)destNode.getParent();
		int index = destParent.getIndex(destNode);
		model.insertNodeInto(curNode, destParent, index + 1);
		
		expandPath(destPath);
		setSelectionPath(new TreePath(curNode.getPath()));
	}

	public void dragOver(DropTargetDragEvent dtde) {
		Point cursorLoc = dtde.getLocation();
		TreePath destPath = getPathForLocation(cursorLoc.x, cursorLoc.y);
		
		if(destPath == null) {
			dtde.rejectDrag();
			return;
		}
		
		PageNode destNode = (PageNode)destPath.getLastPathComponent();
		if(destNode == getRoot() || destNode == curNode)
			dtde.rejectDrag();
		else dtde.acceptDrag(DnDConstants.ACTION_MOVE);
	}
	
	public void dragEnter(DropTargetDragEvent dtde) { /* */ }
	public void dragExit(DropTargetEvent dte) { /* */ }
	public void dropActionChanged(DropTargetDragEvent dtde) { /* */ }
}
